/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.language.syntax.builder;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.modelcc.io.java.Reflection;
import org.modelcc.language.LanguageException;
import org.modelcc.language.factory.SymbolIdentifier;
import org.modelcc.language.factory.ListContent;
import org.modelcc.language.metamodel.SimpleLanguageElement;
import org.modelcc.language.metamodel.MemberCollection;
import org.modelcc.language.metamodel.CompositeLanguageElement;
import org.modelcc.language.metamodel.LanguageModel;
import org.modelcc.language.metamodel.LanguageElement;
import org.modelcc.language.metamodel.LanguageMember;
import org.modelcc.language.syntax.ObjectWrapper;
import org.modelcc.language.syntax.ParserMetadata;
import org.modelcc.language.syntax.RuleSymbol;
import org.modelcc.language.syntax.Symbol;
import org.modelcc.language.syntax.SymbolBuilder;

import test.modelcc.language.types.model.ObjectCaster;

/**
 * Composite symbol builder.
 * 
 * @author Luis Quesada (lquesada@modelcc.org) & Fernando Berzal (fberzal@modelcc.org)
 */
public final class CompositeSymbolBuilder extends SymbolBuilder implements Serializable 
{
    /**
     * Constructor
     */
    public CompositeSymbolBuilder(LanguageModel model) 
    {
    	super(model);
    }
    
    /**
     * Builds a symbol, filling its listData, and validates it.
     * @param t symbol to be built.
     * @param data the parser metadata.
     * @return true if the symbol is valid, false if not
     */
    @Override
    public boolean build(Symbol t, ParserMetadata data) 
    {
        Map<Class,Map<ObjectWrapper,Object>> ids = data.getKeys();
        Map<Object,ObjectWrapper> map = data.getMap();
        SymbolIdentifier eid = (SymbolIdentifier)t.getType();
        CompositeLanguageElement ce = (CompositeLanguageElement) eid.getElement();
        Class c = ce.getElementClass();
        Object o;
        boolean valid = true;

        try {
            o = c.newInstance();
            Set<RuleSymbol> proc = new HashSet<RuleSymbol>();
            Set<Field> filled = new HashSet<Field>();
            for (int i = 0;i < t.size();i++) {
                Symbol s = t.getContent(i);
                RuleSymbol re = t.getElement(i);
                proc.add(re);
                if (re.getLanguageMember()!=null) {
                    LanguageMember ct = re.getLanguageMember();
                    Field fld = Reflection.findField(c,ct.getID());
                    Object list;
                    Method addm;
                    int j;
                    Object[] listData;
                    if (fld != null) {
                        fld.setAccessible(true);
                        if (!ct.getClass().equals(MemberCollection.class)) {
                        	Object content = s.getUserData();
                            fld.set(o,content);
                            if (content!=null) {
                            	filled.add(fld);
                            }
                        }
                        else {
                        	filled.add(fld);
                            MemberCollection mc = (MemberCollection)ct;
                            ListContent listContents = (ListContent) s.getUserData();
                            if (listContents==null) {
                            	throw new LanguageException("CompositeSymbolBuilder: Error while processing "+o);
                            }
                            listData = listContents.getContent();
                            Symbol extra = listContents.getSymbol();
                            RuleSymbol extraRe = listContents.getRuleSymbol();
                            if (extraRe != null) {
	                            LanguageMember extraCt = extraRe.getLanguageMember();
	                            Field extraFld = Reflection.findField(c,extraCt.getID());
	                            filled.add(extraFld);
	                            if (extraFld != null) {
	                            	extraFld.setAccessible(true);
	                            	extraFld.set(o,extra.getUserData());
	                            }
                            }
                            if (listData.length<mc.getMinimumMultiplicity())
                                valid = false;
                            if (mc.getMaximumMultiplicity() != -1) {
                                if (listData.length>mc.getMaximumMultiplicity())
                                    valid = false;
                            }
                            switch (mc.getCollection()) {
                                case LIST:
                                    if (fld.getType().isInterface())
                                            list = ArrayList.class.newInstance();
                                        else
                                            list = fld.getType().newInstance();
                                        addm = list.getClass().getDeclaredMethod("add",Object.class);
                                        if (listData != null)
                                            for (j = 0;j < listData.length;j++)
                                                addm.invoke(list,listData[j]);
                                        fld.set(o,list);
                                        break;
                                    case ARRAY:
                                        list = Array.newInstance(ct.getElementClass(),listData.length);
                                        if (listData != null)
                                            for (j = 0;j < listData.length;j++) {
                                                Array.set(list, j, listData[j]);
                                            }
                                        fld.set(o,list);
                                        break;
                                    case SET:
                                        if (fld.getType().isInterface())
                                            list = HashSet.class.newInstance();
                                        else
                                            list = fld.getType().newInstance();
                                        addm = list.getClass().getDeclaredMethod("add",Object.class);
                                        if (listData != null)
                                            for (j = 0;j < listData.length;j++)
                                                addm.invoke(list,listData[j]);
                                        fld.set(o,list);
                                        break;
                                }
                        }
                    }
                }
            }
            fixOptionals(o,getModel(),filled);
            runSetupMethods(o,ce);
            valid &= runConstraints(o,ce);

            t.setUserData(o);

        } catch (Exception ex) {
            log(Level.SEVERE, null, ex);
            return false;
        }

        if (valid && !ce.getKeyMembers().isEmpty()) {
            Map<ObjectWrapper,Object> idmap = ids.get(c);
            if (idmap == null) {
                idmap = new HashMap<ObjectWrapper,Object>();
                ids.put(c,idmap);
            }
            ObjectWrapper kw = ObjectWrapper.createKeyWrapper(o, getModel(), map);
            if (idmap.containsKey(kw)) {
                log(Level.SEVERE, "Class \"{0}\": Duplicate ID.", new Object[]{c.getCanonicalName()});
            } else {
                idmap.put(kw,o);
            }
        }
        return valid;
    }

    private boolean fixOptionals(Object o, LanguageModel m,Set<Field> filled) throws InstantiationException, IllegalAccessException, NoSuchFieldException,SecurityException, IllegalArgumentException, NoSuchMethodException, InvocationTargetException {
    	if (m.getClassToElement().get(o.getClass()).getClass().equals(SimpleLanguageElement.class)) {
    		Class c = o.getClass();
            SimpleLanguageElement be = (SimpleLanguageElement) m.getClassToElement().get(c);
            try {
                if (be.getValueField() != null) {
                    Field fld = Reflection.findField(c,be.getValueField());
                    if (fld != null) {
                        fld.setAccessible(true);
                        if (fld.getType().equals(String.class))
                        	fld.set(o, ObjectCaster.cast(fld.getType(), ""));
                    }
                }
            } catch (Exception ex) {
                log(Level.SEVERE, null, ex);
                return false;
            }

        } else {
	        CompositeLanguageElement ce = (CompositeLanguageElement)m.getClassToElement().get(o.getClass());
	        Field[] fields = o.getClass().getDeclaredFields();
	        for (int i = 0;i < fields.length;i++) {
	            LanguageMember ct = ce.getFieldToContent().get(fields[i].getName());
	            if (ct != null) {
	                Field fld = Reflection.findField(o.getClass(),ct.getID());
	                if (filled == null || !filled.contains(fields[i])) {
	                    if (ct != null) {
	                        if (!ct.isOptional()) {
	                            if (!ct.getClass().equals(MemberCollection.class)) {
	                                Class c = fields[i].getType();
	                                Object o2;
	                                while (Modifier.isAbstract(c.getModifiers()) && m.getDefaultElement().get(m.getClassToElement().get(c)) != null) {
	                                	c = m.getDefaultElement().get(m.getClassToElement().get(c)).iterator().next().getElementClass();
	                                }
	                                if (!Modifier.isAbstract(c.getModifiers())) {
	                                	o2 = c.newInstance();
		                    	        LanguageElement ee = (LanguageElement)m.getClassToElement().get(o2.getClass());
		                                runSetupMethods(o2,ee);
		                                if (runConstraints(o2,ee)) {
	                                		fixOptionals(o2,m,null);
		                                	fields[i].setAccessible(true);
		                                	fields[i].set(o,o2);
		                                }
		                                	
	                                }
	                            }
	                            else {
	                                MemberCollection mc = (MemberCollection)ct;
	                                Object list;
	                                if (mc.getMinimumMultiplicity() == 0) {
	                                    switch (mc.getCollection()) {
	                                        case LIST:
	                                            if (fld.getType().isInterface())
	                                                    list = ArrayList.class.newInstance();
	                                                else
	                                                    list = fld.getType().newInstance();
	                                                fields[i].setAccessible(true);
	                                                fields[i].set(o,list);
	                                                break;
	                                            case ARRAY:
	                                                list = Array.newInstance(ct.getElementClass(),0);
	                                                fields[i].setAccessible(true);
	                                                fields[i].set(o,list);
	                                                break;
	                                            case SET:
	                                                if (fld.getType().isInterface())
	                                                    list = HashSet.class.newInstance();
	                                                else
	                                                    list = fld.getType().newInstance();
	                                                fields[i].setAccessible(true);
	                                                fields[i].set(o,list);
	                                                break;
	                                    }
	                                }
	                            }
	                        }
	                    }
	                }
	            }
	        }
        }
    	return true;
    }
    

}
